package com.mikesamuel.dateseries;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.mikesamuel.dateseries.shared.DateSeries;
import com.mikesamuel.dateseries.shared.Day;
import com.mikesamuel.dateseries.shared.Freq;
import com.mikesamuel.dateseries.shared.Inference;
import com.mikesamuel.dateseries.shared.WeekDay;

import junit.framework.TestCase;
import static com.mikesamuel.dateseries.shared.WeekDay.*;

public class DateSeriesTest extends TestCase {

  public final void testDayNormalization() {
    assertEquals("1975-12-31", new Day(1975, 12, 31).toString());
    assertEquals("1976-01-01", new Day(1975, 12, 32).toString());
    assertEquals("1975-12-31", new Day(1976, 1, 0).toString());
    assertEquals("1975-12-15", new Day(1976, 0, 15).toString());
    assertEquals("1975-03-01", new Day(1974, 14, 29).toString());
    assertEquals("1976-02-29", new Day(1975, 14, 29).toString());
    assertEquals("1975-02-28", new Day(1975, 3, 0).toString());
    assertEquals("1976-02-29", new Day(1976, 3, 0).toString());
    assertEquals("1975-02-28", new Day(1975, 4, -31).toString());
    assertEquals("1976-02-29", new Day(1976, 4, -31).toString());
    assertEquals("1975-06-01", new Day(1976, 6, 1 - 366).toString());
    assertEquals("1974-05-31", new Day(1975, 6, 1 - 366).toString());
  }

  public final void testDayOfWeek() {
    assertEquals(WE, new Day(2010, 1, 6).dayOfWeek());
    assertEquals(TH, new Day(1988, 7, 21).dayOfWeek());
    assertEquals(SU, new Day(1954, 11, 14).dayOfWeek());
  }

  private void assertDateSeries(
      DateSeries.Specification spec,
      Day first,
      String str,
      Day... days) {
    DateSeries series = new DateSeries(first, spec);
    assertEquals(str, series.toString());
    List<Day> actual = new ArrayList<Day>();
    DateSeries.PeekingDayIterator it = series.iterator();
    for (int i = 0; i < days.length && it.hasNext(); ++i) {
      actual.add(it.next());
    }
    assertEquals("" + Arrays.asList(days), "" + actual);

    // Check that a second iterator produces the same series.
    actual.clear();
    it = series.iterator();
    for (int i = 0; i < days.length && it.hasNext(); ++i) {
      actual.add(it.next());
    }
    assertEquals("" + Arrays.asList(days), "" + actual);

    // Check that the next date is the same as if we convert the iterator back
    // to a series.
    DateSeries paused = it.toDateSeries();
    DateSeries.PeekingDayIterator it2 = paused.iterator();
    assertEquals(str, it.hasNext(), it2.hasNext());
    if (it.hasNext()) {
      assertEquals(str, it.next(), it2.next());
    }

    // Serialize and unserialize the series and retest.
    byte[] bits = series.serialize();
    DateSeries seriesDepickled = DateSeries.deserialize(bits);
    actual.clear();
    it = seriesDepickled.iterator();
    for (int i = 0; i < days.length && it.hasNext(); ++i) {
      actual.add(it.next());
    }
    assertEquals("" + Arrays.asList(days), "" + actual);
  }

  public final void testDateSeries() {
    assertDateSeries(
      new DateSeries.Specification(),
      new Day(2012, 2, 17),
      "Every year on the 17th of Feb.",
      new Day(2012, 2, 17), new Day(2013, 2, 17), new Day(2014, 2, 17),
      new Day(2015, 2, 17), new Day(2016, 2, 17));
    assertDateSeries(
      spec(Freq.MONTHLY),
      new Day(2012, 9, 17),
      "Every month on the 17th",
      new Day(2012, 9, 17), new Day(2012, 10, 17), new Day(2012, 11, 17),
      new Day(2012, 12, 17), new Day(2013, 1, 17));
    assertDateSeries(
      spec(Freq.MONTHLY, new int[] { 15, -16 }),
      new Day(2012, 9, 12),
      "Every month on the 15th, and the 16th to last day",
      new Day(2012, 9, 15), new Day(2012, 10, 15), new Day(2012, 10, 16),
      new Day(2012, 11, 15), new Day(2012, 12, 15), new Day(2012, 12, 16),
      new Day(2013, 1, 15));
    assertDateSeries(
      spec(Freq.MONTHLY, 2, new int[] { 1, 2, 3, 4, -1 }),
      new Day(2012, 9, 12),
      "Every 2 months on the 1st, the 2nd, the 3rd, the 4th, and the last day",
      new Day(2012, 9, 30), new Day(2012, 11, 1), new Day(2012, 11, 2),
      new Day(2012, 11, 3), new Day(2012, 11, 4), new Day(2012, 11, 30));
    assertDateSeries(
      spec(Freq.MONTHLY, new int[] { 1, 2, 3, 4, -1 },
           new WeekDay[] { MO, TU, WE, TH, FR }),
      new Day(2012, 11, 1),
      "Every month on weekdays that are on the 1st, the 2nd, the 3rd, the 4th,"
      + " and the last day",
      new Day(2012, 11, 1), new Day(2012, 11, 2), new Day(2012, 11, 30),
      new Day(2012, 12, 3), new Day(2012, 12, 4), new Day(2012, 12, 31),
      new Day(2013, 1, 1), new Day(2013, 1, 2), new Day(2013, 1, 3),
      new Day(2013, 1, 4), new Day(2013, 1, 31));
    assertDateSeries(
      spec(Freq.WEEKLY, new WeekDay[] { MO, WE, FR }),
      new Day(2012, 7, 1),
      "Every week on Mondays, Wednesdays, and Fridays",
      new Day(2012, 7, 2), new Day(2012, 7, 4), new Day(2012, 7, 6),
      new Day(2012, 7, 9), new Day(2012, 7, 11), new Day(2012, 7, 13));
    assertDateSeries(
      spec(Freq.WEEKLY, 3, new WeekDay[] { TU, TH }),
      new Day(2012, 7, 1),
      "Every 3 weeks on Tuesdays, and Thursdays",
      new Day(2012, 7, 3), new Day(2012, 7, 5), new Day(2012, 7, 24),
      new Day(2012, 7, 26), new Day(2012, 8, 14), new Day(2012, 8, 16),
      new Day(2012, 9, 4));
    assertDateSeries(
      spec(Freq.WEEKLY),
      new Day(2012, 7, 2),
      "Every week on Mondays",
      new Day(2012, 7, 2), new Day(2012, 7, 9), new Day(2012, 7, 16),
      new Day(2012, 7, 23), new Day(2012, 7, 30), new Day(2012, 8, 6));
    assertDateSeries(
      spec(Freq.WEEKLY, 2),
      new Day(2012, 7, 2),
      "Every 2 weeks on Mondays",
      new Day(2012, 7, 2), new Day(2012, 7, 16), new Day(2012, 7, 30),
      new Day(2012, 8, 13), new Day(2012, 8, 27));
    assertDateSeries(
      spec(Freq.DAILY, 3, new int[] { 13 }),
      new Day(2012, 7, 2),
      "Every 3 days on the 13th of the month",
      new Day(2012, 8, 13), new Day(2013, 1, 13), new Day(2013, 4, 13),
      new Day(2013, 5, 13), new Day(2013, 9, 13));
    assertDateSeries(
      spec(Freq.MONTHLY, 6, new int[] { 29 }),
      new Day(2012, 8, 01),
      "Every 6 months on the 29th",
      new Day(2012, 8, 29), new Day(2013, 8, 29), new Day(2014, 8, 29),
      new Day(2015, 8, 29), new Day(2016, 2, 29), new Day(2016, 8, 29));
    assertDateSeries(
      spec(Freq.DAILY, 27),
      new Day(2012, 1, 27),
      "Every 27 days",
      new Day(2012, 1, 27), new Day(2012, 2, 23), new Day(2012, 3, 21),
      new Day(2012, 4, 17), new Day(2012, 5, 14), new Day(2012, 6, 10),
      new Day(2012, 7, 7), new Day(2012, 8, 3), new Day(2012, 8, 30),
      new Day(2012, 9, 26));
  }

  private void assertInference(String golden, Day... picked) {
    Inference inference = new Inference(
        Collections.unmodifiableList(Arrays.asList(picked)), SU);
    String humanLanguageDesc = inference.rule == null
      ? "None" : new DateSeries(picked[0], inference.rule).toString();
    if (inference.confidence < 0.75) {
      humanLanguageDesc += " [low-confidence]";
    }
    assertEquals(golden, humanLanguageDesc);
  }

  public final void testDateSeriesInference() {
    assertInference("None [low-confidence]");
    assertInference(
      "Every week on Tuesdays, and Thursdays",
      new Day(2012, 7, 3), new Day(2012, 7, 5), new Day(2012, 7, 10),
      new Day(2012, 7, 12));
    assertInference(
      "Every 3 months on the 15th",
      new Day(2012, 1, 15), new Day(2012, 4, 15), new Day(2012, 7, 15));
    assertInference(
      "Every 3 months on the last day",
      new Day(2012, 1, 31), new Day(2012, 4, 30), new Day(2012, 7, 31));
    assertInference(
      "Every 27 days",
      new Day(2012, 1, 27), new Day(2012, 2, 23), new Day(2012, 3, 21),
      new Day(2012, 4, 17), new Day(2012, 5, 14));
    assertInference(
      "Every 2 weeks on Fridays",
      new Day(2012, 2, 10), new Day(2012, 2, 24), new Day(2012, 3, 9),
      new Day(2012, 3, 23));
  }


  // A bunch of separate convenience ctors so that we test the defaults as well.
  private DateSeries.Specification spec(Freq f) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    return s;
  }

  private DateSeries.Specification spec(Freq f, int interval) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    s.interval = interval;
    return s;
  }

  private DateSeries.Specification spec(Freq f, int interval, int[] mdays) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    s.interval = interval;
    s.daysOfMonth = mdays;
    return s;
  }

  private DateSeries.Specification spec(Freq f, int[] mdays) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    s.daysOfMonth = mdays;
    return s;
  }

  private DateSeries.Specification spec(Freq f, int[] mdays, WeekDay[] wdays) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    s.daysOfMonth = mdays;
    s.daysOfWeek = wdays;
    return s;
  }

  private DateSeries.Specification spec(Freq f, WeekDay[] wdays) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    s.daysOfWeek = wdays;
    return s;
  }

  private DateSeries.Specification spec(Freq f, int interval, WeekDay[] wdays) {
    DateSeries.Specification s = new DateSeries.Specification();
    s.frequency = f;
    s.interval = interval;
    s.daysOfWeek = wdays;
    return s;
  }
}
